// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.account.fragments

import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback
import net.openid.appauth.ClientSecretPost
import net.openid.appauth.RegistrationRequest
import net.openid.appauth.RegistrationResponse
import net.openid.appauth.ResponseTypeValues
import net.openid.appauth.TokenResponse
import org.acra.ktx.sendSilentlyWithAcra
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.account.AccountActivity
import xyz.apiote.bimba.czwek.account.Client
import xyz.apiote.bimba.czwek.data.sources.Server
import xyz.apiote.bimba.czwek.databinding.FragmentLoginBinding
import xyz.apiote.bimba.czwek.repo.OfflineRepository
import xyz.apiote.bimba.czwek.repo.User
import java.security.MessageDigest
import kotlin.math.max

class LoginFragment : Fragment() {
	private var _binding: FragmentLoginBinding? = null
	private val binding get() = _binding!!
	private lateinit var preferences: SharedPreferences
	private lateinit var authState: AuthState
	private lateinit var server: Server
	private var authError: Exception? = null
	private lateinit var user: User
	private lateinit var _context: Context
	private var client: Client? = null

	companion object {
		const val PREFERENCES_NAME = "shp"
		val CALLBACK = "bimba://callback".toUri()
		const val SCOPES = "openid offline_access email profile"
		const val CLIENT_NAME = "Bimba app (czwek)"
		const val CLIENT_NAME_KEY = "client_name"
		fun newInstance() = LoginFragment()
	}

	@Suppress("unused")
	private val viewModel: LoginViewModel by viewModels()

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
	}

	override fun onCreateView(
		inflater: LayoutInflater, container: ViewGroup?,
		savedInstanceState: Bundle?
	): View {
		super.onCreateView(inflater, container, savedInstanceState)
		_binding = FragmentLoginBinding.inflate(inflater)

		if (context == null) {
			return binding.root
		} else {
			_context = requireContext()
		}

		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			val l = max(windowInsets.displayCutout?.safeInsetLeft ?: 0, insets.left)
			val r = max(windowInsets.displayCutout?.safeInsetRight ?: 0, insets.right)
			val t = max(windowInsets.displayCutout?.safeInsetTop ?: 0, insets.top)
			val b = max(windowInsets.displayCutout?.safeInsetBottom ?: 0, insets.bottom)
			v.updatePadding(left = l, right = r, top = t, bottom = b)
			windowInsets
		}

		preferences = _context.getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)

		preferences.edit(true) {
			putBoolean(AccountActivity.IN_FEEDS_TRANSACTION, true)
		}

		binding.server.editText!!.addTextChangedListener { editable ->
			binding.accountButton.isEnabled = !editable.isNullOrBlank()
			binding.anonymousButton.isEnabled = !editable.isNullOrBlank()
			binding.continueButton.isEnabled = !editable.isNullOrBlank()
		}

		binding.defaultServerSwitch.setOnCheckedChangeListener { _, checked ->
			binding.server.visibility = if (checked) {
				binding.accountButton.isEnabled = true
				binding.anonymousButton.isEnabled = true
				binding.continueButton.isEnabled = true
				View.GONE
			} else {
				View.VISIBLE
			}
		}

		binding.serverInfoButton.setOnClickListener {
			val dialog = MaterialAlertDialogBuilder(_context)
				.setIcon(
					AppCompatResources.getDrawable(
						_context,
						R.drawable.info
					)
				)
				.setTitle(R.string.what_is_a_traffic_server)
				.setMessage(
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
						Html.fromHtml(getString(R.string.server_info), 0)
					} else {
						@Suppress("DEPRECATION")
						Html.fromHtml(getString(R.string.server_info))
					}
				)
				.setPositiveButton(R.string.ok) { _, _ -> }
				.show()
			dialog.findViewById<TextView>(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance()
		}

		binding.reportButton.setOnClickListener {
			authError?.sendSilentlyWithAcra()
			binding.errorText.setText(R.string.report_sent)
			binding.reportButton.visibility = View.GONE
		}

		server = Server.get(_context)
		binding.server.editText!!.setText(server.host)
		binding.defaultServerSwitch.isChecked = server.host == Server.DEFAULT

		binding.accountButton.setOnClickListener {
			startAuth()
		}

		binding.server.editText!!.setOnKeyListener { _, keyCode, event ->
			if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP && binding.continueButton.isVisible) {
				doContinue()
				true
			} else {
				false
			}
		}

		binding.continueButton.setOnClickListener {
			doContinue()
		}

		binding.anonymousButton.setOnClickListener {
			user = User(null, null)
			moveOn()
		}

		if (OfflineRepository(_context).getSuperUser()) {
			binding.continueButton.visibility = View.GONE
			binding.anonymousButton.visibility = View.VISIBLE
			binding.accountButton.visibility = View.VISIBLE
		}
		return binding.root
	}

	@OptIn(ExperimentalStdlibApi::class)
	private fun doContinue() {
		val md = MessageDigest.getInstance("SHA-256")
		md.update(binding.server.editText!!.text.toString().encodeToByteArray())
		val hex = md.digest().toHexString()
		if (hex == "0621b618c336648cd99dcf85bec68b61741c8679a27ef13eef7f91cc24e2eb80") {
			binding.continueButton.visibility = View.GONE
			binding.anonymousButton.visibility = View.VISIBLE
			binding.accountButton.visibility = View.VISIBLE
			binding.server.editText!!.setText(server.host)
			OfflineRepository(_context).saveSuperUser(true)
		} else {
			user = User(null, null)
			moveOn()
		}
	}

	private suspend fun setServer(hostname: String) {
		server.host = hostname
		server.save(_context)
		server.getTraffic(_context, true)
	}

	private fun startAuth() {
		// TODO plusA loading
		binding.errorText.visibility = View.GONE
		binding.reportButton.visibility = View.GONE

		MainScope().launch {
			setServer(
				if (binding.defaultServerSwitch.isChecked) {
					Server.DEFAULT
				} else {
					binding.server.editText!!.text.toString()
				}
			)

			AuthorizationServiceConfiguration.fetchFromIssuer(
				server.traffic!!.authEndpoint.toUri(),
				RetrieveConfigurationCallback { serviceConfiguration, ex ->
					onRetrieveConfiguration(serviceConfiguration, ex)
				}
			)
		}
	}

	private fun onAuthError(exception: Exception) {
		binding.errorText.visibility = View.VISIBLE
		binding.errorText.setText(R.string.error_while_logging_in)
		binding.reportButton.visibility = View.VISIBLE
		authError = exception
	}

	private fun onRetrieveConfiguration(
		config: AuthorizationServiceConfiguration?,
		exception: AuthorizationException?
	) {
		if (exception != null) {
			Log.e("OIDC", "failed to fetch configuration")
			onAuthError(exception)
			return
		}
		authState = AuthState(config!!)
		client = Client.load(_context)
		// TODO plusA version client, if new version -> create new client; changed scopes, &c.
		if (client != null && client!!.authEndpoint == server.traffic!!.authEndpoint) {
			performAuthorization()
		} else {
			val registrationRequest = RegistrationRequest.Builder(
				config,
				listOf(CALLBACK, AccountFragment.LOGOUT_CALLBACK)
			)
				.setAdditionalParameters(
					mapOf(
						CLIENT_NAME_KEY to CLIENT_NAME
					)
				)
				.build()

			val service = AuthorizationService(_context)
			service.performRegistrationRequest(
				registrationRequest
			) { response, ex ->
				onPerformRegistration(response, ex)
			}
		}
	}

	fun onPerformRegistration(response: RegistrationResponse?, ex: AuthorizationException?) {
		if (ex != null) {
			Log.e("OIDC", "failed to register client")
			onAuthError(ex)
			return
		}
		authState.update(response)
		client = Client(response!!.clientId, response.clientSecret!!, server.traffic!!.authEndpoint)
		client!!.save(_context)

		performAuthorization()
	}

	fun performAuthorization() {
		val authRequest = AuthorizationRequest.Builder(
			authState.authorizationServiceConfiguration!!,
			client!!.id,
			ResponseTypeValues.CODE,
			CALLBACK
		)
			.setScope(SCOPES)
			.build()

		val authIntent = AuthorizationService(_context).getAuthorizationRequestIntent(authRequest)
		(activity as AccountActivity?)?.launchLogin(authIntent)

	}

	fun onAuthorization(resp: AuthorizationResponse?, ex: AuthorizationException?) {
		authState.update(resp, ex)
		if (resp != null) {
			AuthorizationService(_context).performTokenRequest(
				resp.createTokenExchangeRequest(),
				ClientSecretPost(client!!.secret)
			) { tokenResponse, tokenException ->
				onTokenRequest(tokenResponse, tokenException)
			}
		} else {
			// TODO plusA stop loading
			Log.e("OIDC", "auth failed: ${ex!!.message}")
			onAuthError(ex)
		}
	}

	fun onTokenRequest(tokenResponse: TokenResponse?, tokenException: AuthorizationException?) {
		authState.update(tokenResponse, tokenException)
		if (tokenResponse != null) {
			user = User(authState, tokenResponse.idToken)
			moveOn()
		} else {
			Log.e("OIDC", "token exchange failed: ${tokenException!!.message}")
			onAuthError(tokenException)
		}
	}

	private fun moveOn() {
		user.save(_context)
		runBlocking {
			binding.server.editText?.text?.toString()?.let { serverAddress -> setServer(serverAddress) }
		}
		(activity as AccountActivity?)?.moveOn(user, server)
	}

}